/*
 * Copyright 2011-2013 Blender Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>

#include "render/buffers.h"
#include "render/camera.h"
#include "device/device.h"
#include "render/scene.h"
#include "render/session.h"
#include "render/integrator.h"

#include "util/util_args.h"
#include "util/util_foreach.h"
#include "util/util_function.h"
#include "util/util_logging.h"
#include "util/util_path.h"
#include "util/util_progress.h"
#include "util/util_string.h"
#include "util/util_time.h"
#include "util/util_transform.h"
#include "util/util_unique_ptr.h"
#include "util/util_version.h"

#ifdef WITH_CYCLES_STANDALONE_GUI
#  include "util/util_view.h"
#endif

#include "app/cycles_xml.h"

CCL_NAMESPACE_BEGIN

struct Options {
  Session *session;
  Scene *scene;
  string filepath;
  int width, height;
  SceneParams scene_params;
  SessionParams session_params;
  bool quiet;
  bool show_help, interactive, pause;
  string output_path;
} options;

static void session_print(const string &str)
{
  /* print with carriage return to overwrite previous */
  printf("\r%s", str.c_str());

  /* add spaces to overwrite longer previous print */
  static int maxlen = 0;
  int len = str.size();
  maxlen = max(len, maxlen);

  for (int i = len; i < maxlen; i++)
    printf(" ");

  /* flush because we don't write an end of line */
  fflush(stdout);
}

static void session_print_status()
{
  string status, substatus;

  /* get status */
  float progress = options.session->progress.get_progress();
  options.session->progress.get_status(status, substatus);

  if (substatus != "")
    status += ": " + substatus;

  /* print status */
  status = string_printf("Progress %05.2f   %s", (double)progress * 100, status.c_str());
  session_print(status);
}

static bool write_render(const uchar *pixels, int w, int h, int channels)
{
  string msg = string_printf("Writing image %s", options.output_path.c_str());
  session_print(msg);

  unique_ptr<ImageOutput> out = unique_ptr<ImageOutput>(ImageOutput::create(options.output_path));
  if (!out) {
    return false;
  }

  ImageSpec spec(w, h, channels, TypeDesc::UINT8);
  if (!out->open(options.output_path, spec)) {
    return false;
  }

  /* conversion for different top/bottom convention */
  out->write_image(
      TypeDesc::UINT8, pixels + (h - 1) * w * channels, AutoStride, -w * channels, AutoStride);

  out->close();

  return true;
}

static BufferParams &session_buffer_params()
{
  static BufferParams buffer_params;
  buffer_params.width = options.width;
  buffer_params.height = options.height;
  buffer_params.full_width = options.width;
  buffer_params.full_height = options.height;

  return buffer_params;
}

static void scene_init()
{
  options.scene = new Scene(options.scene_params, options.session->device);

  /* Read XML */
  xml_read_file(options.scene, options.filepath.c_str());

  /* Camera width/height override? */
  if (!(options.width == 0 || options.height == 0)) {
    options.scene->camera->width = options.width;
    options.scene->camera->height = options.height;
  }
  else {
    options.width = options.scene->camera->width;
    options.height = options.scene->camera->height;
  }

  /* Calculate Viewplane */
  options.scene->camera->compute_auto_viewplane();
}

static void session_init()
{
  options.session_params.write_render_cb = write_render;
  options.session = new Session(options.session_params);

  if (options.session_params.background && !options.quiet)
    options.session->progress.set_update_callback(function_bind(&session_print_status));
#ifdef WITH_CYCLES_STANDALONE_GUI
  else
    options.session->progress.set_update_callback(function_bind(&view_redraw));
#endif

  /* load scene */
  scene_init();
  options.session->scene = options.scene;

  options.session->reset(session_buffer_params(), options.session_params.samples);
  options.session->start();
}

static void session_exit()
{
  if (options.session) {
    delete options.session;
    options.session = NULL;
  }

  if (options.session_params.background && !options.quiet) {
    session_print("Finished Rendering.");
    printf("\n");
  }
}

#ifdef WITH_CYCLES_STANDALONE_GUI
static void display_info(Progress &progress)
{
  static double latency = 0.0;
  static double last = 0;
  double elapsed = time_dt();
  string str, interactive;

  latency = (elapsed - last);
  last = elapsed;

  double total_time, sample_time;
  string status, substatus;

  progress.get_time(total_time, sample_time);
  progress.get_status(status, substatus);
  float progress_val = progress.get_progress();

  if (substatus != "")
    status += ": " + substatus;

  interactive = options.interactive ? "On" : "Off";

  str = string_printf(
      "%s"
      "        Time: %.2f"
      "        Latency: %.4f"
      "        Progress: %05.2f"
      "        Average: %.4f"
      "        Interactive: %s",
      status.c_str(),
      total_time,
      latency,
      (double)progress_val * 100,
      sample_time,
      interactive.c_str());

  view_display_info(str.c_str());

  if (options.show_help)
    view_display_help();
}

static void display()
{
  static DeviceDrawParams draw_params = DeviceDrawParams();

  options.session->draw(session_buffer_params(), draw_params);

  display_info(options.session->progress);
}

static void motion(int x, int y, int button)
{
  if (options.interactive) {
    Transform matrix = options.session->scene->camera->matrix;

    /* Translate */
    if (button == 0) {
      float3 translate = make_float3(x * 0.01f, -(y * 0.01f), 0.0f);
      matrix = matrix * transform_translate(translate);
    }

    /* Rotate */
    else if (button == 2) {
      float4 r1 = make_float4((float)x * 0.1f, 0.0f, 1.0f, 0.0f);
      matrix = matrix * transform_rotate(DEG2RADF(r1.x), make_float3(r1.y, r1.z, r1.w));

      float4 r2 = make_float4(y * 0.1f, 1.0f, 0.0f, 0.0f);
      matrix = matrix * transform_rotate(DEG2RADF(r2.x), make_float3(r2.y, r2.z, r2.w));
    }

    /* Update and Reset */
    options.session->scene->camera->matrix = matrix;
    options.session->scene->camera->need_update = true;
    options.session->scene->camera->need_device_update = true;

    options.session->reset(session_buffer_params(), options.session_params.samples);
  }
}

static void resize(int width, int height)
{
  options.width = width;
  options.height = height;

  if (options.session) {
    /* Update camera */
    options.session->scene->camera->width = width;
    options.session->scene->camera->height = height;
    options.session->scene->camera->compute_auto_viewplane();
    options.session->scene->camera->need_update = true;
    options.session->scene->camera->need_device_update = true;

    options.session->reset(session_buffer_params(), options.session_params.samples);
  }
}

static void keyboard(unsigned char key)
{
  /* Toggle help */
  if (key == 'h')
    options.show_help = !(options.show_help);

  /* Reset */
  else if (key == 'r')
    options.session->reset(session_buffer_params(), options.session_params.samples);

  /* Cancel */
  else if (key == 27)  // escape
    options.session->progress.set_cancel("Canceled");

  /* Pause */
  else if (key == 'p') {
    options.pause = !options.pause;
    options.session->set_pause(options.pause);
  }

  /* Interactive Mode */
  else if (key == 'i')
    options.interactive = !(options.interactive);

  /* Navigation */
  else if (options.interactive && (key == 'w' || key == 'a' || key == 's' || key == 'd')) {
    Transform matrix = options.session->scene->camera->matrix;
    float3 translate;

    if (key == 'w')
      translate = make_float3(0.0f, 0.0f, 0.1f);
    else if (key == 's')
      translate = make_float3(0.0f, 0.0f, -0.1f);
    else if (key == 'a')
      translate = make_float3(-0.1f, 0.0f, 0.0f);
    else if (key == 'd')
      translate = make_float3(0.1f, 0.0f, 0.0f);

    matrix = matrix * transform_translate(translate);

    /* Update and Reset */
    options.session->scene->camera->matrix = matrix;
    options.session->scene->camera->need_update = true;
    options.session->scene->camera->need_device_update = true;

    options.session->reset(session_buffer_params(), options.session_params.samples);
  }

  /* Set Max Bounces */
  else if (options.interactive && (key == '0' || key == '1' || key == '2' || key == '3')) {
    int bounce;
    switch (key) {
      case '0':
        bounce = 0;
        break;
      case '1':
        bounce = 1;
        break;
      case '2':
        bounce = 2;
        break;
      case '3':
        bounce = 3;
        break;
      default:
        bounce = 0;
        break;
    }

    options.session->scene->integrator->max_bounce = bounce;

    /* Update and Reset */
    options.session->scene->integrator->need_update = true;

    options.session->reset(session_buffer_params(), options.session_params.samples);
  }
}
#endif

static int files_parse(int argc, const char *argv[])
{
  if (argc > 0)
    options.filepath = argv[0];

  return 0;
}

static void options_parse(int argc, const char **argv)
{
  options.width = 0;
  options.height = 0;
  options.filepath = "";
  options.session = NULL;
  options.quiet = false;

  /* device names */
  string device_names = "";
  string devicename = "CPU";
  bool list = false;

  /* List devices for which support is compiled in. */
  vector<DeviceType> types = Device::available_types();
  foreach (DeviceType type, types) {
    if (device_names != "")
      device_names += ", ";

    device_names += Device::string_from_type(type);
  }

  /* shading system */
  string ssname = "svm";

  /* parse options */
  ArgParse ap;
  bool help = false, debug = false, version = false;
  int verbosity = 1;

  ap.options("Usage: cycles [options] file.xml",
             "%*",
             files_parse,
             "",
             "--device %s",
             &devicename,
             ("Devices to use: " + device_names).c_str(),
#ifdef WITH_OSL
             "--shadingsys %s",
             &ssname,
             "Shading system to use: svm, osl",
#endif
             "--background",
             &options.session_params.background,
             "Render in background, without user interface",
             "--quiet",
             &options.quiet,
             "In background mode, don't print progress messages",
             "--samples %d",
             &options.session_params.samples,
             "Number of samples to render",
             "--output %s",
             &options.output_path,
             "File path to write output image",
             "--threads %d",
             &options.session_params.threads,
             "CPU Rendering Threads",
             "--width  %d",
             &options.width,
             "Window width in pixel",
             "--height %d",
             &options.height,
             "Window height in pixel",
             "--tile-width %d",
             &options.session_params.tile_size.x,
             "Tile width in pixels",
             "--tile-height %d",
             &options.session_params.tile_size.y,
             "Tile height in pixels",
             "--list-devices",
             &list,
             "List information about all available devices",
#ifdef WITH_CYCLES_LOGGING
             "--debug",
             &debug,
             "Enable debug logging",
             "--verbose %d",
             &verbosity,
             "Set verbosity of the logger",
#endif
             "--help",
             &help,
             "Print help message",
             "--version",
             &version,
             "Print version number",
             NULL);

  if (ap.parse(argc, argv) < 0) {
    fprintf(stderr, "%s\n", ap.geterror().c_str());
    ap.usage();
    exit(EXIT_FAILURE);
  }

  if (debug) {
    util_logging_start();
    util_logging_verbosity_set(verbosity);
  }

  if (list) {
    vector<DeviceInfo> devices = Device::available_devices();
    printf("Devices:\n");

    foreach (DeviceInfo &info, devices) {
      printf("    %-10s%s%s\n",
             Device::string_from_type(info.type).c_str(),
             info.description.c_str(),
             (info.display_device) ? " (display)" : "");
    }

    exit(EXIT_SUCCESS);
  }
  else if (version) {
    printf("%s\n", CYCLES_VERSION_STRING);
    exit(EXIT_SUCCESS);
  }
  else if (help || options.filepath == "") {
    ap.usage();
    exit(EXIT_SUCCESS);
  }

  if (ssname == "osl")
    options.scene_params.shadingsystem = SHADINGSYSTEM_OSL;
  else if (ssname == "svm")
    options.scene_params.shadingsystem = SHADINGSYSTEM_SVM;

#ifndef WITH_CYCLES_STANDALONE_GUI
  options.session_params.background = true;
#endif

  /* Use progressive rendering */
  options.session_params.progressive = true;

  /* find matching device */
  DeviceType device_type = Device::type_from_string(devicename.c_str());
  vector<DeviceInfo> devices = Device::available_devices(DEVICE_MASK(device_type));

  bool device_available = false;
  if (!devices.empty()) {
    options.session_params.device = devices.front();
    device_available = true;
  }

  /* handle invalid configurations */
  if (options.session_params.device.type == DEVICE_NONE || !device_available) {
    fprintf(stderr, "Unknown device: %s\n", devicename.c_str());
    exit(EXIT_FAILURE);
  }
#ifdef WITH_OSL
  else if (!(ssname == "osl" || ssname == "svm")) {
    fprintf(stderr, "Unknown shading system: %s\n", ssname.c_str());
    exit(EXIT_FAILURE);
  }
  else if (options.scene_params.shadingsystem == SHADINGSYSTEM_OSL &&
           options.session_params.device.type != DEVICE_CPU) {
    fprintf(stderr, "OSL shading system only works with CPU device\n");
    exit(EXIT_FAILURE);
  }
#endif
  else if (options.session_params.samples < 0) {
    fprintf(stderr, "Invalid number of samples: %d\n", options.session_params.samples);
    exit(EXIT_FAILURE);
  }
  else if (options.filepath == "") {
    fprintf(stderr, "No file path specified\n");
    exit(EXIT_FAILURE);
  }

  /* For smoother Viewport */
  options.session_params.start_resolution = 64;
}

CCL_NAMESPACE_END

using namespace ccl;

int main(int argc, const char **argv)
{
  util_logging_init(argv[0]);
  path_init();
  options_parse(argc, argv);

#ifdef WITH_CYCLES_STANDALONE_GUI
  if (options.session_params.background) {
#endif
    session_init();
    options.session->wait();
    session_exit();
#ifdef WITH_CYCLES_STANDALONE_GUI
  }
  else {
    string title = "Cycles: " + path_filename(options.filepath);

    /* init/exit are callback so they run while GL is initialized */
    view_main_loop(title.c_str(),
                   options.width,
                   options.height,
                   session_init,
                   session_exit,
                   resize,
                   display,
                   keyboard,
                   motion);
  }
#endif

  return 0;
}
